/* Copyright (c) 2022-2023, Arm Limited and Contributors. All rights reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

#include <assert.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#include "nimble/nimble_npl.h"
#include "nimble/nimble_npl_os.h"
#include "syscfg/syscfg.h"

#include "cmsis_os2.h"

#define DEFAULT_EVENTQ_SIZE    32  // Inspired from the freertos NPL
#define DEFAULT_SEM_MAX        128 // Inspired from the freertos NPL
#define MILLISECONDS_IN_SECOND 1000

static ble_npl_error_t convert_cmsis_error_to_npl_error(osStatus_t cmsis_error)
{
    switch (cmsis_error) {
        case osOK:
            return BLE_NPL_OK;
        case osError:
            return BLE_NPL_ERROR;
        case osErrorTimeout:
            return BLE_NPL_TIMEOUT;
        case osErrorResource:
            return BLE_NPL_EBUSY;
        case osErrorParameter:
            return BLE_NPL_INVALID_PARAM;
        case osErrorNoMemory:
            return BLE_NPL_ENOMEM;
        case osErrorISR:
            return BLE_NPL_ERR_IN_ISR;
        default:
            return BLE_NPL_ERROR;
    }
}

//------- generic -------

bool ble_npl_os_started(void)
{
    return osKernelRunning == osKernelGetState();
}

void *ble_npl_get_current_task_id(void)
{
    return osThreadGetId();
}

//----- event queue -----

void ble_npl_eventq_init(struct ble_npl_eventq *event_queue)
{
    assert(event_queue);
    event_queue->id = osMessageQueueNew(DEFAULT_EVENTQ_SIZE, sizeof(struct ble_npl_event *), NULL);
    if (!event_queue->id) {
        abort();
    }
}

struct ble_npl_event *ble_npl_eventq_get(struct ble_npl_eventq *event_queue, ble_npl_time_t timeout)
{
    assert(event_queue);
    struct ble_npl_event *event_pointer;
    // Fetch a pointer to the event struct that's allocated in a mempool
    osStatus_t status = osMessageQueueGet(event_queue->id, &event_pointer, NULL, timeout);
    if (osOK != status) {
        return NULL;
    }
    return event_pointer;
}

void ble_npl_eventq_put(struct ble_npl_eventq *event_queue, struct ble_npl_event *event_pointer)
{
    assert(event_queue);
    assert(event_pointer);
    // Store a pointer to the event struct that's allocated in a mempool
    osStatus_t status = osMessageQueuePut(event_queue->id, &event_pointer, 0, osWaitForever);
    if (osOK != status) {
        abort();
    }
    event_pointer->queued = true;
}

void ble_npl_eventq_remove(struct ble_npl_eventq *event_queue, struct ble_npl_event *event)
{
    assert(event_queue);
    assert(event);
    // TODO The CMSIS API don't support removing messages/event from the queue
    // This mean we would need to either implement our own queuing system,
    // or dequeue/requeue, which is a quite inelegant solution.
    // For now we'll assume that leaving an event in the queue is not breaking anything
    // and just do a nop
    // This is not currently used : the only place that might use it for the BLE host is
    // in ble_gap.c, and that's behind a #ifdef that's not yet enabled :
    // #if MYNEWT_VAL(BLE_PERIODIC_ADV)

// syscfg.h is only needed for this macro. It should be removed from here and the makefile
// when this is dealt with.
#if MYNEWT_VAL(BLE_PERIODIC_ADV)
#error "ble_npl_eventq_remove is not implemented yet but code that uses it have been enabled\n"
#endif
}

bool ble_npl_eventq_is_empty(struct ble_npl_eventq *event_queue)
{
    assert(event_queue);
    return (osMessageQueueGetCount(event_queue->id) == 0);
}

//-------- event --------

void ble_npl_event_init(struct ble_npl_event *event, ble_npl_event_fn *callback, void *context)
{
    assert(event);
    event->queued = false;
    event->callback = callback;
    event->context = context;
}

bool ble_npl_event_is_queued(struct ble_npl_event *event)
{
    assert(event);
    return event->queued;
}

void *ble_npl_event_get_arg(struct ble_npl_event *event)
{
    assert(event);
    return event->context;
}

void ble_npl_event_set_arg(struct ble_npl_event *event, void *context)
{
    assert(event);
    event->context = context;
}

void ble_npl_event_run(struct ble_npl_event *event)
{
    assert(event);
    assert(event->callback);
    event->callback(event);
}

//-------- mutex --------

ble_npl_error_t ble_npl_mutex_init(struct ble_npl_mutex *mutex)
{
    if (mutex == NULL) {
        return BLE_NPL_INVALID_PARAM;
    }

    mutex->id = osMutexNew(NULL);

    return (mutex->id == NULL) ? BLE_NPL_ERROR : BLE_NPL_OK;
}

ble_npl_error_t ble_npl_mutex_pend(struct ble_npl_mutex *mutex, ble_npl_time_t timeout)
{
    if (mutex == NULL) {
        return BLE_NPL_INVALID_PARAM;
    }
    return convert_cmsis_error_to_npl_error(osMutexAcquire(mutex->id, timeout));
}

ble_npl_error_t ble_npl_mutex_release(struct ble_npl_mutex *mutex)
{
    if (mutex == NULL) {
        return BLE_NPL_INVALID_PARAM;
    }
    return convert_cmsis_error_to_npl_error(osMutexRelease(mutex->id));
}

//------ semaphore ------

ble_npl_error_t ble_npl_sem_init(struct ble_npl_sem *semaphore, uint16_t tokens)
{
    if (semaphore == NULL) {
        return BLE_NPL_INVALID_PARAM;
    }

    semaphore->id = osSemaphoreNew(DEFAULT_SEM_MAX, tokens, NULL);

    return (semaphore->id == NULL) ? BLE_NPL_ERROR : BLE_NPL_OK;
}

ble_npl_error_t ble_npl_sem_pend(struct ble_npl_sem *semaphore, ble_npl_time_t timeout)
{
    if (semaphore == NULL) {
        return BLE_NPL_INVALID_PARAM;
    }
    osStatus_t status = osSemaphoreAcquire(semaphore->id, timeout);
    return convert_cmsis_error_to_npl_error(status);
}

ble_npl_error_t ble_npl_sem_release(struct ble_npl_sem *semaphore)
{
    if (semaphore == NULL) {
        return BLE_NPL_INVALID_PARAM;
    }
    osStatus_t status = osSemaphoreRelease(semaphore->id);
    return convert_cmsis_error_to_npl_error(status);
}

uint16_t ble_npl_sem_get_count(struct ble_npl_sem *semaphore)
{
    assert(semaphore);
    uint32_t count = osSemaphoreGetCount(semaphore->id);
    return (uint16_t)(count);
}

//------- callout -------

static void on_callout_timer_expiry(void *context)
{
    struct ble_npl_callout *callout = context;
    assert(callout);
    /*
     * Two use cases :  an event_queue is given and it periodicaly adds an event to the event_queue, or
     *                  no event_queue is given and it periodicaly process an event
     */
    if (callout->event_queue) {
        ble_npl_eventq_put(callout->event_queue, &callout->event);
    } else {
        callout->event.callback(&callout->event);
    }
    callout->expiry = ble_npl_time_get() + callout->period;
}

void ble_npl_callout_init(struct ble_npl_callout *callout,
                          struct ble_npl_eventq *event_queue,
                          ble_npl_event_fn *event_callback,
                          void *event_arguments)
{
    assert(callout);
    assert(event_queue);
    assert(event_callback);
    memset(callout, 0, sizeof(*callout));
    // Create periodic timer
    callout->id = osTimerNew(on_callout_timer_expiry, osTimerPeriodic, callout, NULL);
    if (!callout->id) {
        abort();
    }
    callout->event_queue = event_queue;
    ble_npl_event_init(&callout->event, event_callback, event_arguments);
}

ble_npl_error_t ble_npl_callout_reset(struct ble_npl_callout *callout, ble_npl_time_t ticks)
{
    if (callout == NULL || ticks == 0) {
        return BLE_NPL_INVALID_PARAM;
    }

    osKernelLock();
    osStatus_t status = osTimerStart(callout->id, ticks);

    if (status == osOK) {
        callout->period = ticks;
        callout->expiry = ble_npl_time_get() + ticks;
    }
    osKernelUnlock();

    return convert_cmsis_error_to_npl_error(status);
}

void ble_npl_callout_stop(struct ble_npl_callout *callout)
{
    assert(callout);
    if (callout->id) {
        osStatus_t status = osTimerStop(callout->id);
        if (status != osOK) {
            abort();
        }
    }
}

bool ble_npl_callout_is_active(struct ble_npl_callout *callout)
{
    assert(callout);
    return osTimerIsRunning(callout->id);
}

// Despite the name, this returns the expiry : the tick at which the timer will fire, and not the timer periodicity
ble_npl_time_t ble_npl_callout_get_ticks(struct ble_npl_callout *callout)
{
    assert(callout);
    return callout->expiry;
}

ble_npl_time_t ble_npl_callout_remaining_ticks(struct ble_npl_callout *callout, ble_npl_time_t now)
{
    assert(callout);

    if (callout->expiry < now) {
        // A counter overflow occurred
        return callout->expiry + 1 + (UINT32_MAX - now);
    }

    return callout->expiry - now;
}

void ble_npl_callout_set_arg(struct ble_npl_callout *callout, void *context)
{
    assert(callout);
    callout->event.context = context;
}

//---- time functions ----

ble_npl_time_t ble_npl_time_get(void)
{
    return osKernelGetTickCount();
}

ble_npl_error_t ble_npl_time_ms_to_ticks(uint32_t ms, ble_npl_time_t *out_ticks)
{
    if (out_ticks == NULL) {
        return BLE_NPL_INVALID_PARAM;
    }
    uint64_t ticks = (osKernelGetTickFreq() * (uint64_t)ms) / MILLISECONDS_IN_SECOND;
    if (ticks > UINT32_MAX) {
        return BLE_NPL_EINVAL;
    }
    *out_ticks = (ble_npl_time_t)ticks;
    return BLE_NPL_OK;
}

ble_npl_error_t ble_npl_time_ticks_to_ms(ble_npl_time_t ticks, uint32_t *out_ms)
{
    if (out_ms == NULL) {
        return BLE_NPL_INVALID_PARAM;
    }
    uint64_t ms = ((uint64_t)ticks * MILLISECONDS_IN_SECOND) / osKernelGetTickFreq();
    if (ms > UINT32_MAX) {
        return BLE_NPL_EINVAL;
    }
    *out_ms = (uint32_t)ms;
    return BLE_NPL_OK;
}

ble_npl_time_t ble_npl_time_ms_to_ticks32(uint32_t ms)
{
    return (ms * osKernelGetTickFreq()) / MILLISECONDS_IN_SECOND;
}

uint32_t ble_npl_time_ticks_to_ms32(ble_npl_time_t ticks)
{
    return (ticks * MILLISECONDS_IN_SECOND) / osKernelGetTickFreq();
}

void ble_npl_time_delay(ble_npl_time_t ticks)
{
    osStatus_t status = osDelay(ticks);
    if (status != osOK) {
        abort();
    }
}

//------ critical ------

#if NIMBLE_CFG_CONTROLLER
#error "NimBLE controler not yet supported in the NPL"
void ble_npl_hw_set_isr(int irqn, void (*addr)(void))
{
    // This function is not needed unless we start supporting the NimBLE controller
}
#endif

uint32_t ble_npl_hw_enter_critical(void)
{
    osKernelLock();
    // We knowingly don't check the return status of the lock to stay in line with other NPL implementations
    return 0;
}

void ble_npl_hw_exit_critical(uint32_t ctx)
{
    (void)ctx;
    osKernelUnlock();
    // We knowingly don't check the return status of the unlock to stay in line with other NPL implementations
}

bool ble_npl_hw_is_in_critical(void)
{
    return osKernelGetState() == osKernelLocked;
}
